package util;

import java.awt.geom.Point2D;


/**
 * This class represents a force applied in a specific direction and magnitude.
 *
 * @author Robert c. Duvall modified by Mark Hoffman and Mark Govea
 */
public class Vector {
    /**
     * The angle in degrees of a vector pointing to the left.
     */
    public static final double ANGLE_RIGHT = 0;
    /**
     * The angle in degrees of a vector pointing up.
     */
    public static final double ANGLE_UP = 270;
    /**
     * The angle in degrees of a vector pointing to the right.
     */
    public static final double ANGLE_LEFT = 180;
    /**
     * The angle in degrees of a vector pointing down.
     */
    public static final double ANGLE_DOWN = 90;

    private static final double OFFSET = 0.001;
    private static final double COMPARISON_THRESHOLD = 0.000001;
    private static final double FULL_TURN = 360;
    private static final double HALF_TURN = 180;
    // angle in degrees
    private double myAngle;
    // "speed" in pixels per second
    private double myMagnitude;

    /**
     * Create a powerless force, i.e., with no magnitude.
     */
    public Vector () {
        this(0, 0);
    }

    /**
     * Create a force in the given direction with the given magnitude.
     *
     * @param angle direction of force
     * @param magnitude strength of force
     */
    public Vector (double angle, double magnitude) {
        setDirection(angle);
        setMagnitude(magnitude);
    }

    /**
     * Create a force whose direction and magnitude are determined by the
     * direction and distance between the two given points.
     *
     * @param source point reflecting position of force
     * @param target point reflection the relative magnitude and direction of
     *        the force
     */
    public Vector (Point2D source, Point2D target) {
        this(angleBetween(target, source), distanceBetween(target, source));
    }

    /**
     * Create a force that is identical to the given other force.
     *
     * @param f force to copy
     */
    public Vector (Vector f) {
        this(f.getDirection(), f.getMagnitude());
    }

    /**
     * Reset this force to zero.
     */
    public void reset () {
        setDirection(0);
        setMagnitude(0);
    }

    /**
     * Returns this force's magnitude (in pixels).
     */
    public double getMagnitude () {
        return myMagnitude;
    }

    /**
     * Returns this force's magnitude relative to the given other force. More
     * formally, returns the magnitude of this force projected onto the given
     * other force.
     *
     * @param f reference force
     */
    public double getRelativeMagnitude (Vector f) {
        return -getMagnitude() * Math.cos(Math.toRadians(getAngleBetween(f)));
    }

    /**
     * Scales this force's magnitude by the given change value.
     * <UL>
     * <LI>A value of 1 leaves the magnitude unchanged
     * <LI>Values less than 1 reduce the magnitude
     * <LI>Values greater than 1 increase the magnitude
     * </UL>
     *
     * @param change factor to scale the force vector
     */
    public void scale (double change) {
        setMagnitude(getMagnitude() * change);
    }

    /**
     * Sets this force's magnitude to the given value.
     */
    protected void setMagnitude (double value) {
        myMagnitude = value;
    }

    /**
     * Returns this force's direction (in degrees).
     */
    public double getDirection () {
        // standardize between -360 and +360 (keep 360, -360, and 0 as distinct
        // values)
        double sign = (myAngle < 0) ? 1 : -1;
        return ((myAngle + sign * OFFSET) % FULL_TURN) - sign * OFFSET;
    }

    /**
     * Returns the angle between this force and the given other force.
     *
     * @param f other force
     */
    public double getAngleBetween (Vector f) {
        return getDirection() - f.getDirection();
    }

    /**
     * Adjusts this force's direction by the given change value.
     *
     * @param change amount of direction rotation in degrees
     */
    public void turn (double change) {
        setDirection(getDirection() + change);
    }

    /**
     * Sets this force's direction to the given value.
     */
    protected void setDirection (double value) {
        myAngle = value;
    }

    /**
     * Returns the change in the X direction represented by this force.
     */
    public double getXChange () {
        return getMagnitude() * Math.cos(Math.toRadians(getDirection()));
    }

    /**
     * Returns the change in the Y direction represented by this force.
     */
    public double getYChange () {
        return getMagnitude() * Math.sin(Math.toRadians(getDirection()));
    }

    /**
     * Returns a force that is the sum of this force and the given other force.
     *
     * @param f force to sum
     */
    public void sum (Vector f) {
        double dx = getXChange() + f.getXChange();
        double dy = getYChange() + f.getYChange();
        setDirection(angleBetween(dx, dy));
        setMagnitude(distanceBetween(dx, dy));
    }

    /**
     * Returns a force that is the difference between this force and the given
     * other force.
     *
     * @param f force to subtract from this one
     */
    public void difference (Vector f) {
        f.negate();
        sum(f);
    }

    /**
     * Returns a force of the same magnitude, but in the opposite direction as
     * this force.
     */
    public void negate () {
        turn(HALF_TURN);
    }

    /**
     * Returns the average of this force with the given other force.
     *
     * @param f force with which to average
     */
    public Vector average (Vector f) {
        return new Vector((getDirection() + f.getDirection()) / 2.0,
                (getMagnitude() + f.getMagnitude()) / 2.0);
    }

    /**
     * Return true if this force has the same magnitude and direction
     * as the given other force.
     *
     * @param force to check equality
     */
    @Override
    public boolean equals (Object force) {
        try {
            Vector other = (Vector) force;
            return (Math.abs(getMagnitude() - other.getMagnitude()) < COMPARISON_THRESHOLD)
                    && (Math.abs(getDirection() - other.getDirection()) < COMPARISON_THRESHOLD);
        }
        catch (ClassCastException e) {
            return false;
        }
    }

    /**
     * Returns this force's values formatted as a string.
     */
    @Override
    public String toString () {
        return String.format("(%1.2f, %1.2f)", getDirection(), getMagnitude());
    }

    /**
     * Returns the distance between given two points.
     *
     * @param p1 destination point
     * @param p2 origin point
     */
    public static double distanceBetween (Point2D p1, Point2D p2) {
        return distanceBetween(p1.getX() - p2.getX(), p1.getY() - p2.getY());
    }

    /**
     * Returns the distance represented by the given dx and dy.
     *
     * @param dx difference in x value of the two points
     * @param dy difference in y value of the two points
     */
    public static double distanceBetween (double dx, double dy) {
        return Math.sqrt(dx * dx + dy * dy);
    }

    /**
     * Returns the angle between the given two points.
     *
     * @param p1 destination point
     * @param p2 origin point
     */
    public static double angleBetween (Point2D p1, Point2D p2) {
        return angleBetween(p1.getX() - p2.getX(), p1.getY() - p2.getY());
    }

    /**
     * Returns the angle represented by the given dx and dy.
     *
     * @param dx difference in x value of the two points
     * @param dy difference in y value of the two points
     */
    public static double angleBetween (double dx, double dy) {
        return Math.toDegrees(Math.atan2(dy, dx));
    }
}
